// QCodeEditor
#include "QCodeEditor.hpp"
#include "QFramedTextAttribute.hpp"
#include "QLineNumberArea.hpp"
#include "QPythonHighlighter.hpp"
#include "QStyleSyntaxHighlighter.hpp"
#include "QSyntaxStyle.hpp"

// Qt
#include <QAbstractItemView>
#include <QAbstractTextDocumentLayout>
#include <QCompleter>
#include <QCursor>
#include <QFontDatabase>
#include <QMimeData>
#include <QPaintEvent>
#include <QScrollBar>
#include <QShortcut>
#include <QTextBlock>
#include <QTextCharFormat>

// PythonQt
#include <QStringListModel>

static QVector<QPair<QString, QString>> parentheses = {
    {"(", ")"}, {"{", "}"}, {"[", "]"}, {"\"", "\""}, {"'", "'"}};

QCodeEditor::QCodeEditor(QWidget *widget)
    : QTextEdit(widget), m_highlighter(nullptr), m_syntaxStyle(nullptr),
      m_lineNumberArea(new QLineNumberArea(this)),
      m_completer(new QCompleter(this)),
      m_framedAttribute(new QFramedTextAttribute(this)),
      m_autoIndentation(true), m_autoParentheses(true), m_replaceTab(true),
      m_tabReplace(QString(4, ' ')) {
  initDocumentLayoutHandlers();
  initFont();
  performConnections();
  setSyntaxStyle(QSyntaxStyle::defaultStyle());
  py = PythonQt::self();
  _context = py->getMainModule();
  connect(m_completer, QOverload<const QString &>::of(&QCompleter::activated),
          this, &QCodeEditor::insertCompletion);
}

void QCodeEditor::initDocumentLayoutHandlers() {
  document()->documentLayout()->registerHandler(QFramedTextAttribute::type(),
                                                m_framedAttribute);
}

void QCodeEditor::initFont() {
  auto fnt = QFontDatabase::systemFont(QFontDatabase::FixedFont);
  fnt.setFixedPitch(true);
  fnt.setPointSize(10);

  setFont(fnt);
}

void QCodeEditor::performConnections() {
  connect(document(), &QTextDocument::blockCountChanged, this,
          &QCodeEditor::updateLineNumberAreaWidth);

  connect(verticalScrollBar(), &QScrollBar::valueChanged,
          [this](int) { m_lineNumberArea->update(); });

  connect(this, &QTextEdit::cursorPositionChanged, this,
          &QCodeEditor::updateExtraSelection);

  connect(this, &QTextEdit::selectionChanged, this,
          &QCodeEditor::onSelectionChanged);
}

void QCodeEditor::setHighlighter(QStyleSyntaxHighlighter *highlighter) {
  if (m_highlighter) {
    m_highlighter->setDocument(nullptr);
  }

  m_highlighter = highlighter;

  if (m_highlighter) {
    m_highlighter->setSyntaxStyle(m_syntaxStyle);
    m_highlighter->setDocument(document());
  }
}

void QCodeEditor::setSyntaxStyle(QSyntaxStyle *style) {
  m_syntaxStyle = style;

  m_framedAttribute->setSyntaxStyle(m_syntaxStyle);
  m_lineNumberArea->setSyntaxStyle(m_syntaxStyle);

  if (m_highlighter) {
    m_highlighter->setSyntaxStyle(m_syntaxStyle);
  }

  updateStyle();
}

void QCodeEditor::updateStyle() {
  if (m_highlighter) {
    m_highlighter->rehighlight();
  }

  if (m_syntaxStyle) {
    auto currentPalette = palette();

    // Setting text format/color
    currentPalette.setColor(
        QPalette::ColorRole::Text,
        m_syntaxStyle->getFormat("Text").foreground().color());

    // Setting common background
    currentPalette.setColor(
        QPalette::Base, m_syntaxStyle->getFormat("Text").background().color());

    // Setting selection color
    currentPalette.setColor(
        QPalette::Highlight,
        m_syntaxStyle->getFormat("Selection").background().color());

    setPalette(currentPalette);
  }

  updateExtraSelection();
}

void QCodeEditor::onSelectionChanged() {
  auto selected = textCursor().selectedText();

  auto cursor = textCursor();

  // Cursor is null if setPlainText was called.
  if (cursor.isNull()) {
    return;
  }

  cursor.movePosition(QTextCursor::MoveOperation::Left);
  cursor.select(QTextCursor::SelectionType::WordUnderCursor);

  QSignalBlocker blocker(this);
  m_framedAttribute->clear(cursor);

  if (selected.size() > 1 && cursor.selectedText() == selected) {
    auto backup = textCursor();

    // Perform search selecting
    handleSelectionQuery(cursor);

    setTextCursor(backup);
  }
}

void QCodeEditor::resizeEvent(QResizeEvent *e) {
  QTextEdit::resizeEvent(e);

  updateLineGeometry();
}

void QCodeEditor::updateLineGeometry() {
  QRect cr = contentsRect();
  m_lineNumberArea->setGeometry(QRect(
      cr.left(), cr.top(), m_lineNumberArea->sizeHint().width(), cr.height()));
}

void QCodeEditor::updateLineNumberAreaWidth(int) {
  setViewportMargins(m_lineNumberArea->sizeHint().width(), 0, 0, 0);
}

void QCodeEditor::updateLineNumberArea(const QRect &rect) {
  m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->sizeHint().width(),
                           rect.height());
  updateLineGeometry();

  if (rect.contains(viewport()->rect())) {
    updateLineNumberAreaWidth(0);
  }
}

void QCodeEditor::handleSelectionQuery(QTextCursor cursor) {

  auto searchIterator = cursor;
  searchIterator.movePosition(QTextCursor::Start);
  searchIterator = document()->find(cursor.selectedText(), searchIterator);
  while (searchIterator.hasSelection()) {
    m_framedAttribute->frame(searchIterator);

    searchIterator = document()->find(cursor.selectedText(), searchIterator);
  }
}

void QCodeEditor::updateExtraSelection() {
  QList<QTextEdit::ExtraSelection> extra;

  highlightCurrentLine(extra);
  highlightParenthesis(extra);

  setExtraSelections(extra);
}

void QCodeEditor::highlightParenthesis(
    QList<QTextEdit::ExtraSelection> &extraSelection) {
  auto currentSymbol = charUnderCursor();
  auto prevSymbol = charUnderCursor(-1);

  for (auto &pair : parentheses) {
    int direction;

    QChar counterSymbol;
    QChar activeSymbol;
    auto position = textCursor().position();

    if (pair.first == currentSymbol) {
      direction = 1;
      counterSymbol = pair.second[0];
      activeSymbol = currentSymbol;
    } else if (pair.second == prevSymbol) {
      direction = -1;
      counterSymbol = pair.first[0];
      activeSymbol = prevSymbol;
      position--;
    } else {
      continue;
    }

    auto counter = 1;

    while (counter != 0 && position > 0 &&
           position < (document()->characterCount() - 1)) {
      // Moving position
      position += direction;

      auto character = document()->characterAt(position);
      // Checking symbol under position
      if (character == activeSymbol) {
        ++counter;
      } else if (character == counterSymbol) {
        --counter;
      }
    }

    auto format = m_syntaxStyle->getFormat("Parentheses");

    // Found
    if (counter == 0) {
      ExtraSelection selection{};

      auto directionEnum = direction < 0 ? QTextCursor::MoveOperation::Left
                                         : QTextCursor::MoveOperation::Right;

      selection.format = format;
      selection.cursor = textCursor();
      selection.cursor.clearSelection();
      selection.cursor.movePosition(
          directionEnum, QTextCursor::MoveMode::MoveAnchor,
          std::abs(textCursor().position() - position));

      selection.cursor.movePosition(QTextCursor::MoveOperation::Right,
                                    QTextCursor::MoveMode::KeepAnchor, 1);

      extraSelection.append(selection);

      selection.cursor = textCursor();
      selection.cursor.clearSelection();
      selection.cursor.movePosition(directionEnum,
                                    QTextCursor::MoveMode::KeepAnchor, 1);

      extraSelection.append(selection);
    }

    break;
  }
}

void QCodeEditor::highlightCurrentLine(
    QList<QTextEdit::ExtraSelection> &extraSelection) {
  if (!isReadOnly()) {
    QTextEdit::ExtraSelection selection{};

    selection.format = m_syntaxStyle->getFormat("CurrentLine");
    selection.format.setForeground(QBrush());
    selection.format.setProperty(QTextFormat::FullWidthSelection, true);
    selection.cursor = textCursor();
    selection.cursor.clearSelection();

    extraSelection.append(selection);
  }
}

void QCodeEditor::paintEvent(QPaintEvent *e) {
  updateLineNumberArea(e->rect());
  QTextEdit::paintEvent(e);
}

int QCodeEditor::getFirstVisibleBlock() {
  // Detect the first block for which bounding rect - once translated
  // in absolute coordinated - is contained by the editor's text area

  // Costly way of doing but since "blockBoundingGeometry(...)" doesn't
  // exists for "QTextEdit"...

  QTextCursor curs = QTextCursor(document());
  curs.movePosition(QTextCursor::Start);
  for (int i = 0; i < document()->blockCount(); ++i) {
    QTextBlock block = curs.block();

    QRect r1 = viewport()->geometry();
    QRect r2 = document()
                   ->documentLayout()
                   ->blockBoundingRect(block)
                   .translated(viewport()->geometry().x(),
                               viewport()->geometry().y() -
                                   verticalScrollBar()->sliderPosition())
                   .toRect();

    if (r1.intersects(r2)) {
      return i;
    }

    curs.movePosition(QTextCursor::NextBlock);
  }

  return 0;
}

bool QCodeEditor::proceedCompleterBegin(QKeyEvent *e) {
  if (m_completer && m_completer->popup()->isVisible()) {
    switch (e->key()) {
    case Qt::Key_Enter:
    case Qt::Key_Return: { // added by wingsummer
      if (!m_completer->popup()->currentIndex().isValid()) {
        insertCompletion(m_completer->currentCompletion());
        m_completer->popup()->hide();
        e->accept();
      }
      e->ignore();
      return true;
      break;
    }
    case Qt::Key_Escape:
    case Qt::Key_Tab:
    case Qt::Key_Backtab:
      e->ignore();
      return true; // let the completer do default behavior
    default:
      break;
    }
  }

  // todo: Replace with modifiable QShortcut
  auto isShortcut =
      ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_Space);

  return !(!m_completer || !isShortcut);
}

void QCodeEditor::proceedCompleterEnd(QKeyEvent *e) {
  auto ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);

  if (!m_completer || (ctrlOrShift && e->text().isEmpty()) ||
      e->key() == Qt::Key_Delete) {
    return;
  }

  static QString eow(R"(~!@#$%^&*()_+{}|:"<>?,./;'[]\-=)");

  auto isShortcut =
      ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_Space);
  auto completionPrefix = wordUnderCursor();

  if (!isShortcut && (e->text().isEmpty() || completionPrefix.length() < 2 ||
                      eow.contains(e->text().right(1)))) {
    m_completer->popup()->hide();
    return;
  }

  //  if (completionPrefix != m_completer->completionPrefix()) {
  //    m_completer->setCompletionPrefix(completionPrefix);
  //    m_completer->popup()->setCurrentIndex(
  //        m_completer->completionModel()->index(0, 0));
  //  }

  //  auto cursRect = cursorRect();
  //  cursRect.setWidth(
  //      m_completer->popup()->sizeHintForColumn(0) +
  //      m_completer->popup()->verticalScrollBar()->sizeHint().width());

  handleTabCompletion();
}

void QCodeEditor::keyPressEvent(QKeyEvent *e) {
#if QT_VERSION >= 0x050A00
  const int defaultIndent =
      int(tabStopDistance() / fontMetrics().averageCharWidth());
#else
  const int defaultIndent = tabStopWidth() / fontMetrics().averageCharWidth();
#endif

  auto completerSkip = proceedCompleterBegin(e);

  if (!completerSkip) {
    if (m_replaceTab && e->key() == Qt::Key_Tab &&
        e->modifiers() == Qt::NoModifier) {
      insertPlainText(m_tabReplace);
      return;
    }

    // Auto indentation
    int indentationLevel = getIndentationSpaces();

#if QT_VERSION >= 0x050A00
    int tabCounts = int(indentationLevel * fontMetrics().averageCharWidth() /
                        tabStopDistance());
#else
    int tabCounts =
        indentationLevel * fontMetrics().averageCharWidth() / tabStopWidth();
#endif

    // Have Qt Edior like behaviour, if {|} and enter is pressed indent the two
    // parenthesis
    if (m_autoIndentation &&
        (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) &&
        charUnderCursor() == '}' && charUnderCursor(-1) == '{') {
      int charsBack = 0;
      insertPlainText("\n");

      if (m_replaceTab)
        insertPlainText(QString(indentationLevel + defaultIndent, ' '));
      else
        insertPlainText(QString(tabCounts + 1, '\t'));

      insertPlainText("\n");
      charsBack++;

      if (m_replaceTab) {
        insertPlainText(QString(indentationLevel, ' '));
        charsBack += indentationLevel;
      } else {
        insertPlainText(QString(tabCounts, '\t'));
        charsBack += tabCounts;
      }

      while (charsBack--)
        moveCursor(QTextCursor::MoveOperation::Left);
      return;
    }

    // Shortcut for moving line to left
    if (m_replaceTab && e->key() == Qt::Key_Backtab) {
      indentationLevel = std::min(indentationLevel, m_tabReplace.size());

      auto cursor = textCursor();

      cursor.movePosition(QTextCursor::MoveOperation::StartOfLine);
      cursor.movePosition(QTextCursor::MoveOperation::Right,
                          QTextCursor::MoveMode::KeepAnchor, indentationLevel);

      cursor.removeSelectedText();
      return;
    }

    QTextEdit::keyPressEvent(e);

    if (m_autoIndentation &&
        (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter)) {
      if (m_replaceTab)
        insertPlainText(QString(indentationLevel, ' '));
      else
        insertPlainText(QString(tabCounts, '\t'));
    }

    if (m_autoParentheses) {
      for (auto &&el : parentheses) {
        // Inserting closed brace
        if (el.first == e->text()) {
          insertPlainText(el.second);
          moveCursor(QTextCursor::MoveOperation::Left);
          break;
        }

        // If it's close brace - check parentheses
        if (el.second == e->text()) {
          auto symbol = charUnderCursor();

          if (symbol == el.second) {
            textCursor().deletePreviousChar();
            moveCursor(QTextCursor::MoveOperation::Right);
          }

          break;
        }
      }
    }
  }

  proceedCompleterEnd(e);
}

void QCodeEditor::handleTabCompletion() {
  QTextCursor textCursor = this->textCursor();
  int pos = textCursor.position();
  textCursor.movePosition(QTextCursor::StartOfLine);
  textCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
  int startPos = textCursor.selectionStart();

  int offset = pos - startPos;
  QString text = textCursor.selectedText();

  QString textToComplete;
  int cur = offset;
  while (cur--) {
    QChar c = text.at(cur);
    if (c.isLetterOrNumber() || c == '.' || c == '_') {
      textToComplete.prepend(c);
    } else {
      break;
    }
  }

  QString lookup;
  QString compareText = textToComplete;
  int dot = compareText.lastIndexOf('.');
  if (dot != -1) {
    lookup = compareText.mid(0, dot);
    compareText = compareText.mid(dot + 1, offset);
  }
  if (!lookup.isEmpty() || !compareText.isEmpty()) {
    compareText = compareText.toLower();
    QStringList found;
    QStringList l = py->introspection(_context, lookup, PythonQt::Anything);
    Q_FOREACH (QString n, l) {
      if (n.toLower().startsWith(compareText)) {
        found << n;
      }
    }

    if (!found.isEmpty()) {
      m_completer->setCompletionPrefix(compareText);
      m_completer->setCompletionMode(QCompleter::PopupCompletion);
      m_completer->setModel(new QStringListModel(found, m_completer));
      m_completer->setCaseSensitivity(Qt::CaseInsensitive);
      QTextCursor c = this->textCursor();
      c.movePosition(QTextCursor::StartOfWord);
      QRect cr = cursorRect(c);
      cr.setWidth(
          m_completer->popup()->sizeHintForColumn(0) +
          m_completer->popup()->verticalScrollBar()->sizeHint().width());
      cr.translate(0, 8);
      m_completer->complete(cr);
    } else {
      m_completer->popup()->hide();
    }
  } else {
    m_completer->popup()->hide();
  }
}

void QCodeEditor::setAutoIndentation(bool enabled) {
  m_autoIndentation = enabled;
}

bool QCodeEditor::autoIndentation() const { return m_autoIndentation; }

void QCodeEditor::setAutoParentheses(bool enabled) {
  m_autoParentheses = enabled;
}

bool QCodeEditor::autoParentheses() const { return m_autoParentheses; }

void QCodeEditor::setTabReplace(bool enabled) { m_replaceTab = enabled; }

bool QCodeEditor::tabReplace() const { return m_replaceTab; }

void QCodeEditor::setTabReplaceSize(int val) {
  m_tabReplace.clear();

  m_tabReplace.fill(' ', val);
}

int QCodeEditor::tabReplaceSize() const { return m_tabReplace.size(); }

void QCodeEditor::focusInEvent(QFocusEvent *e) {
  if (m_completer) {
    m_completer->setWidget(this);
  }

  QTextEdit::focusInEvent(e);
}

void QCodeEditor::insertCompletion(QString s) {
  if (m_completer->widget() != this) {
    return;
  }

  auto tc = textCursor();
  tc.select(QTextCursor::SelectionType::WordUnderCursor);
  tc.insertText(s);
  setTextCursor(tc);
}

QChar QCodeEditor::charUnderCursor(int offset) const {
  auto block = textCursor().blockNumber();
  auto index = textCursor().positionInBlock();
  auto text = document()->findBlockByNumber(block).text();

  index += offset;

  if (index < 0 || index >= text.size()) {
    return {};
  }

  return text[index];
}

QString QCodeEditor::wordUnderCursor() const {
  auto tc = textCursor();
  tc.select(QTextCursor::WordUnderCursor);
  return tc.selectedText();
}

void QCodeEditor::insertFromMimeData(const QMimeData *source) {
  insertPlainText(source->text());
}

int QCodeEditor::getIndentationSpaces() {
  auto blockText = textCursor().block().text();

  int indentationLevel = 0;

  for (auto i = 0;
       i < blockText.size() && QString("\t ").contains(blockText[i]); ++i) {
    if (blockText[i] == ' ') {
      indentationLevel++;
    } else {
#if QT_VERSION >= 0x050A00
      indentationLevel += tabStopDistance() / fontMetrics().averageCharWidth();
#else
      indentationLevel += tabStopWidth() / fontMetrics().averageCharWidth();
#endif
    }
  }

  return indentationLevel;
}
